package pkg.andru.widget;

import pkg.andru.util.Log;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.GestureDetector.OnDoubleTapListener;
import android.view.GestureDetector.OnGestureListener;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.ScaleGestureDetector.OnScaleGestureListener;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.DecelerateInterpolator;

/**
 * better image view, like the imageview, but support multi-touch and zoom in
 * and out. </br><strong>Notes: some function has some limit</strong>
 * 
 * @author idiottiger
 * @version 1.0
 * 
 */
public class BetterImageView extends View implements OnScaleGestureListener, OnGestureListener, OnDoubleTapListener {

    public static final String LOG_TAG = "BetterImageView";

    private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);

    static final int MAX_MOVE_P_X = 400;

    static final int DEFAULT_DURATION = 250, SMALL_DURATION = 50;

    private float mScaleFactor = 1.f;

    private Bitmap mBitmap;

    private Matrix matrix = new Matrix(), savedMatrix = new Matrix();

    private boolean isInited;

    private float mPosX, mPosY;

    @SuppressWarnings("unused")
    private float leftX, rightX;

    private float minScale, maxScale, defaultScale;

    private int canvasWidth, canvasHeight, bitmapWidth, bitmapHeight;

    // We can be in one of these 3 states
    static final int NONE = 0, DRAG = 1, ZOOM = 2;

    private int mode = NONE;

    // Remember some things for zooming
    PointF startPoint = new PointF();

    private ScaleGestureDetector mScaleDetector;

    private GestureDetector mDetector;

    private boolean isScaling, isScaleAnimCanStarted;

    private AbsoluteTranslateAnimation mTranslateAnimation;

    private AbsoluteScaleAnimation mScaleAnimation;

    private AnimationListener mAnimationListener;

    float[] src = new float[2];

    float[] dst = new float[2];

    /**
     * 
     * @param context
     */
    public BetterImageView(Context context) {
        this(context, null);
    }

    /**
     * can set in the xml file, but not supprot {@link View} attrs, not
     * imageview.
     * 
     * @param context
     * @param attrs
     */
    public BetterImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mScaleDetector = new ScaleGestureDetector(context, this);
        mDetector = new GestureDetector(context, this);
        mAnimationListener = new TranslateAnimationListener();
        mDetector.setOnDoubleTapListener(this);
    }

    /**
     * set bitmap tp image view
     * 
     * @param map
     */
    public void setBitmap(Bitmap map) {
        reset();
        mBitmap = map;
        postInvalidate();
    }

    /**
     * return bitmap in the imageview
     * 
     * @return
     */
    public Bitmap getBitmap() {
        return mBitmap;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (isBitmapAvailable()) {
            init(canvas);
            updateAnimation();
            drawDrawable(canvas);
            checkAnimation();
        }
    }

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        canvasWidth = MeasureSpec.getSize(widthMeasureSpec);
        canvasHeight = MeasureSpec.getSize(heightMeasureSpec);
        isInited = false;

        Log.i(LOG_TAG, "w:" + canvasWidth + ",h:" + canvasHeight);
    }

    /**
     * reset the image view's all states, the bitmap will be recycled.
     */
    public void reset() {
        // recycle the bitmap
        if (isBitmapAvailable()) {
            mBitmap.recycle();
            mBitmap = null;
        }
        if (matrix != null) {
            matrix.reset();
        }
        if (savedMatrix != null) {
            savedMatrix.reset();
        }
        if (mTranslateAnimation != null) {
            mTranslateAnimation.reset();
        }
        if (mScaleAnimation != null) {
            mScaleAnimation.reset();
        }
        isInited = false;
    }

    /**
     * update view, if the bitmap change or the ori change
     */
    public void updateView() {
        makeViewLaterRefresh();
        postInvalidate();
    }

    private void makeViewLaterRefresh() {
        if (matrix != null) {
            matrix.reset();
        }
        if (savedMatrix != null) {
            savedMatrix.reset();
        }
        isInited = false;
    }

    private void drawDrawable(Canvas canvas) {
        if (isBitmapAvailable()) {
            canvas.drawBitmap(mBitmap, matrix, mPaint);
        }
    }

    /**
     * check the bitmap can drawable
     * 
     * @return true or false
     */
    public boolean isBitmapAvailable() {
        return mBitmap != null && !mBitmap.isRecycled();
    }

    private void init(Canvas canvas) {
        if (!isInited) {

            // get w, h
            bitmapWidth = mBitmap.getWidth();
            bitmapHeight = mBitmap.getHeight();

            // get default scale
            float scale = Math.min((float) canvasWidth / bitmapWidth, (float) canvasHeight / bitmapHeight);
            float scale2 = Math.min((float) bitmapWidth / canvasWidth, (float) bitmapHeight / canvasHeight);

            mScaleFactor = scale;
            defaultScale = scale;

            minScale = mScaleFactor;
            maxScale = Math.max(mScaleFactor * 3, scale2);

            mPosX = (canvasWidth - scale * bitmapWidth) / 2 / scale;
            mPosY = (canvasHeight - scale * bitmapHeight) / 2 / scale;

            if (matrix != null) {
                matrix.reset();
                savedMatrix.reset();
            } else {
                matrix = new Matrix();
                savedMatrix = new Matrix();
            }
            updateMatrix();
            isInited = true;
        }
    }

    private void updateMatrix() {
        matrix.setTranslate(mPosX, mPosY);
        matrix.postScale(mScaleFactor, mScaleFactor);
        savedMatrix.reset();
        savedMatrix.set(matrix);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        mScaleDetector.onTouchEvent(event);

        if (!isScaling) {

            if (mDetector != null) {
                mDetector.onTouchEvent(event);
            }

            final int action = event.getAction() & MotionEvent.ACTION_MASK;

            switch (action) {

            case MotionEvent.ACTION_DOWN:
                savedMatrix.set(matrix);
                startPoint.set(event.getX(), event.getY());
                mode = DRAG;
                break;

            case MotionEvent.ACTION_UP:
                checkReset();
                mode = NONE;
                break;

            case MotionEvent.ACTION_MOVE:
                if (mode == DRAG) {
                    matrix.set(savedMatrix);
                    final int pointerCount = event.getPointerCount();
                    for (int p = 0; p < pointerCount; p++) {
                        float move_x = event.getX(p) - startPoint.x;
                        float move_y = event.getY(p) - startPoint.y;
                        if (move_x != 0 || move_y != 0) {
                            checkMove(move_x, move_y);
                        }
                    }
                }
                break;
            }
        }

        return true;
    }

    private void updatePosition() {
        src[0] = src[1] = 0;
        matrix.mapPoints(dst, src);
        mPosX = dst[0];
        mPosY = dst[1];
    }

    private void checkReset() {
        // if (mScaleFactor == defaultScale || !isMovingUp)

        float scale = mScaleFactor;
        if (mScaleFactor >= maxScale || mScaleFactor <= minScale) {
            mScaleFactor = (mScaleFactor >= maxScale ? maxScale : minScale);
        }

        updatePosition();

        float to_x = (canvasWidth - scale * bitmapWidth) / 2;
        float to_y = (canvasHeight - scale * bitmapHeight) / 2;

        boolean isWrongLocation = (to_x != mPosX || to_y != mPosY);

        // mean need small or bigger
        if (scale != mScaleFactor) {
            initScaleAnimation(scale, mScaleFactor);
            if (isWrongLocation) {
                initTranslateAnimation(mPosX, to_x, mPosY, to_y, SMALL_DURATION, false);
                Log.i(LOG_TAG, "anim:" + mPosX + "->" + to_x + "," + mPosY + "->" + to_y);
                mTranslateAnimation.setAnimationListener(mAnimationListener);
                mTranslateAnimation.start();
            } else {
                mScaleAnimation.start();
            }
        } else {
            float tw = mScaleFactor * bitmapWidth, th = mScaleFactor * bitmapHeight;

            float tx = mPosX, ty = mPosY;
            float abs_dh = Math.abs(canvasHeight - th);
            float dw = canvasWidth - tw;

            if (th < canvasHeight) {
                ty = abs_dh / 2;
            } else if (mPosY > 0) {
                ty = 0;
                // ty = mPosY > 0 ? 0 : -abs_dh;
            } else if (mPosY < -abs_dh) {
                ty = -abs_dh;
            }

            if (tw <= canvasWidth) {
                tx = (canvasWidth - tw) / 2;
            } else if (mPosX > 0) {
                tx = 0;
            } else if (mPosX < dw) {
                tx = dw;
            }

            if (tx != mPosX || ty != mPosY) {
                initTranslateAnimation(mPosX, tx, mPosY, ty);
                invalidate();
            }
        }

        savedMatrix.reset();
        savedMatrix.set(matrix);

        invalidate();

    }

    /**
     * need add left and right go fun
     * 
     * @param move_x
     * @param move_y
     */
    private void checkMove(float move_x, float move_y) {
        matrix.postTranslate(move_x, mScaleFactor == defaultScale ? 0 : move_y);
        invalidate();

        updatePosition();
        // move to right
        if (move_x > 0 && mPosX > canvasWidth / 2) {

        }
    }

    private void initTranslateAnimation(float fromx, float tox, float fromy, float toy, long duration, boolean autoStart) {
        if (mTranslateAnimation == null) {
            mTranslateAnimation = new AbsoluteTranslateAnimation(fromx, tox, fromy, toy);
            mTranslateAnimation.setInterpolator(new DecelerateInterpolator(0.515f));
        } else {
            mTranslateAnimation.reset();
            mTranslateAnimation.setTransformation(fromx, tox, fromy, toy);
        }
        mTranslateAnimation.setDuration(duration);
        isScaleAnimCanStarted = false;
        mTranslateAnimation.setAnimationListener(null);
        if (autoStart) {
            mTranslateAnimation.start();
        }
    }

    private void initTranslateAnimation(float fromx, float tox, float fromy, float toy, long duration) {
        initTranslateAnimation(fromx, tox, fromy, toy, duration, true);
    }

    private void initTranslateAnimation(float fromx, float tox, float fromy, float toy) {
        initTranslateAnimation(fromx, tox, fromy, toy, DEFAULT_DURATION);
    }

    private void initScaleAnimation(float fromScaleFactor, float toScaleFactor) {
        if (mScaleAnimation == null) {
            mScaleAnimation = new AbsoluteScaleAnimation(fromScaleFactor, toScaleFactor, fromScaleFactor, toScaleFactor);
            mScaleAnimation.setInterpolator(new DecelerateInterpolator(0.515f));
            mScaleAnimation.setDuration(DEFAULT_DURATION);
        } else {
            mScaleAnimation.reset();
            mScaleAnimation.setScaleformation(fromScaleFactor, toScaleFactor, fromScaleFactor, toScaleFactor);
        }
        isScaleAnimCanStarted = false;
        // mTranslateAnimation.setAnimationListener(null);
    }

    private void updateAnimation() {
        if (isTranslateAnimationOk()) {
            float[] values = new float[2];
            mTranslateAnimation.getCurrentPostTranslate(values);
            matrix.postTranslate(values[0], values[1]);
        }
        if (isScaleAnimationOk()) {
            float[] scales = new float[2];
            mScaleAnimation.getCurrentScale(scales);
            matrix.postScale(scales[0], scales[1], canvasWidth / 2, canvasHeight / 2);
        }
    }

    private boolean isTranslateAnimationOk() {
        return mTranslateAnimation != null && !mTranslateAnimation.hasEnded();
    }

    private boolean isScaleAnimationOk() {
        return isScaleAnimCanStarted && mScaleAnimation != null && !mScaleAnimation.hasEnded();
    }

    private void checkAnimation() {
        if (isAnimationNotEnd()) {
            invalidate();
        }
    }

    private boolean isAnimationNotEnd() {
        return isTranslateAnimationOk() || isScaleAnimationOk();
    }

    public boolean onScale(ScaleGestureDetector detector) {
        float scale = detector.getScaleFactor();
        float scale_cp = 0;
        if (Float.isInfinite(scale) || Float.isNaN(scale))
            return true;

        scale_cp = mScaleFactor * scale;
        if (scale_cp >= maxScale || scale_cp <= minScale - 0.1f) {
            // Log.i(LOG_TAG, "c:" + scale_cp + ",r:" + mScaleFactor);
            return true;
        }
        mScaleFactor = scale_cp;
        // Log.i(LOG_TAG, "on scale:" + mScaleFactor);
        matrix.postScale(scale, scale, detector.getFocusX(), detector.getFocusY());

        invalidate();
        return true;
    }

    public boolean onScaleBegin(ScaleGestureDetector detector) {
        Log.i(LOG_TAG, "onScaleBegin...");
        isScaling = true;
        mode = ZOOM;
        return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {
        isScaling = false;
        Log.i(LOG_TAG, "onScaleEnd...");
    }

    public boolean onDown(MotionEvent e) {
        return false;
    }

    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        return false;
    }

    public void onLongPress(MotionEvent e) {

    }

    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        return false;
    }

    public void onShowPress(MotionEvent e) {

    }

    public boolean onSingleTapUp(MotionEvent e) {
        return false;
    }

    class TranslateAnimationListener implements AnimationListener {

        @Override
        public void onAnimationEnd(Animation animation) {
            if (mScaleAnimation != null) {
                isScaleAnimCanStarted = true;
                mScaleAnimation.start();
                postInvalidate();
                Log.i(LOG_TAG, "mScaleAnimation begin");
            }
        }

        @Override
        public void onAnimationRepeat(Animation animation) {

        }

        @Override
        public void onAnimationStart(Animation animation) {

        }

    }

    @Override
    public boolean onDoubleTap(MotionEvent e) {
        Log.i(LOG_TAG, "onDoubleTap:" + defaultScale + "," + maxScale + ",current:" + mScaleFactor);
        float finalFactor = mScaleFactor;
        if (mScaleFactor > minScale) {
            finalFactor = minScale;
        } else if (mScaleFactor == minScale) {
            finalFactor = maxScale;
        }

        if (finalFactor == maxScale) {
            float scale = finalFactor / mScaleFactor;
            mScaleFactor = finalFactor;
            matrix.postScale(scale, scale, e.getX(), e.getY());

            invalidate();
        } else {
            updateView();
        }
        return false;
    }

    @Override
    public boolean onDoubleTapEvent(MotionEvent e) {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) {
        // TODO Auto-generated method stub
        return false;
    }

    public static interface OnMoveListener {
        public boolean onMoveToRightOutOfArea();
    }

}
